Skip to content

feat(core/hooks): add 'useIsClient' hook#219

Merged
kimyouknow merged 12 commits into
toss:mainfrom
sukvvon:feat/add-useIsClient
Mar 6, 2026
Merged

feat(core/hooks): add 'useIsClient' hook#219
kimyouknow merged 12 commits into
toss:mainfrom
sukvvon:feat/add-useIsClient

Conversation

@sukvvon
Copy link
Copy Markdown
Contributor

@sukvvon sukvvon commented Apr 25, 2025

Overview

Add useIsClient hook that returns true only after client-side hydration, preventing SSR/CSR hydration mismatches.

  • Migrated to monorepo structure (packages/core/src/hooks/useIsClient/)
  • Added bilingual docs (English + Korean)
  • Added changeset (patch)

Checklist

  • Did you write the test code?
  • Have you run yarn run fix to format and lint the code and docs?
  • Have you run yarn run test:coverage to make sure there is no uncovered line?
  • Did you write the JSDoc?

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 25, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 100.00%. Comparing base (bea638d) to head (3869fb5).
Report is 18 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##              main      #219   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           33        34    +1     
  Lines          836       842    +6     
  Branches       254       256    +2     
=========================================
+ Hits           836       842    +6     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@seungrodotlee
Copy link
Copy Markdown
Contributor

We have similar discuss here: #202

@sukvvon
Copy link
Copy Markdown
Contributor Author

sukvvon commented May 7, 2025

@seungrodotlee

useIsClient is necessary in an SSR (Server-Side Rendering) environment for the following reasons:

  • To safely control access to browser-only APIs (like window, localStorage, etc.)
  • To conditionally render UI that should only appear on the client side
  • To prevent hydration mismatches between server-rendered HTML and client-rendered React components

@seungrodotlee
Copy link
Copy Markdown
Contributor

I understand that distinguishing between the server and client environments is important,
but I'm not quite sure why this must be provided as a hook.
What are the advantages compared to providing it as a constant?

const isClient = typeof window !== 'undefined';

@sukvvon
Copy link
Copy Markdown
Contributor Author

sukvvon commented May 8, 2025

@seungrodotlee

Using typeof window !== 'undefined' gives a static value before rendering, which can lead to hydration mismatches between SSR and CSR. On the other hand, useIsClient leverages useEffect, so it only turns true after the component mounts on the client.

This allows:

  • Safe use of browser-only APIs
  • Conditional rendering of client-only UI
  • React-compliant, state-driven transitions between SSR and CSR

In React, a hook like useIsClient is a safer and more reliable approach than using a constant.

@JaeSang1998
Copy link
Copy Markdown

JaeSang1998 commented May 8, 2025

@sukvvon I think using useSyncExternalStore would be a better approach since it avoids unnecessary re-renders compared to useEffect. Also, i think it follows React's recommended approach in a more standardized way, instead of relying on typeof window checks. What do you think?

import { useSyncExternalStore } from 'react';

const client = {
  subscribe: () => () => {},
  getClientSnapshot: () => true,
  getServerSnapshot: () => false,
};

const useIsClient = () => {
  const isClient = useSyncExternalStore(
    client.subscribe,
    client.getClientSnapshot,
    client.getServerSnapshot
  );

  return isClient;
};

export default useIsClient;

@sukvvon
Copy link
Copy Markdown
Contributor Author

sukvvon commented May 8, 2025

@JaeSang1998

useSyncExternalStore is available starting from React 18.
Currently, peerDependencies is set to "react": "*", which allows installation in React 17 environments where this hook doesn’t exist and can cause runtime errors.

  • If we need to support React 17, we need to install the use-sync-external-store (shim).
  • If not, it’s better to set peerDependencies to "react": ">=18.0.0" for clarity.

If our library targets React 18+, the latter is the cleaner and safer approach.

@kimyouknow
Copy link
Copy Markdown
Collaborator

@sukvvon Thank you for your patience — this PR has been open for a while and we really appreciate both the implementation and the thoughtful discussion (especially the useSyncExternalStore trade-off).

We've reviewed the code and confirmed useIsClient is a good fit for the core package. A few notes:

No overlap with isServer() — they serve different purposes:

  • isServer(): Static function, no re-render
  • useIsClient(): Hook that triggers re-render after hydration, prevents mismatch

Implementation approach is correctuseState + useEffect is the right choice given our peerDependencies: "react": "*" (React 17 support). This matches the usehooks-ts approach. If we ever bump the minimum to React 18+, we'll consider migrating to useSyncExternalStore for the perf benefit (no extra re-render).

To merge, we need:

  1. Rebase on latest main — the project is now a monorepo (Project Direction Update: Platform-Independent Hooks Focus #329), so the hook goes in packages/core/src/hooks/
  2. Add Korean docs (ko/useIsClient.md)
  3. Add changeset (yarn changeset → patch, guide)

Looking forward to getting this merged!

@kimyouknow
Copy link
Copy Markdown
Collaborator

@zztnrudzz13 FYI — once this lands, we're planning to:

  • Add isServer() as a public utility in core
  • Remove the duplicate isServer() from @react-simplikit/mobile and import from core instead

This aligns with our architecture rule (mobile depends on core, not the other way around). Will handle as a follow-up PR.

@sukvvon
Copy link
Copy Markdown
Contributor Author

sukvvon commented Mar 3, 2026

@kimyouknow 👌

@sukvvon sukvvon changed the title feat(hooks): add 'useIsClient' feat(core/hooks): add 'useIsClient' hook Mar 3, 2026
@sukvvon
Copy link
Copy Markdown
Contributor Author

sukvvon commented Mar 4, 2026

@kimyouknow Thank you for the detailed review!

All three items have been addressed in 90781a2 and d81ab5c:

  1. Rebased on latest main (migrated to packages/core/src/hooks/useIsClient/)
  2. Added Korean docs (ko/useIsClient.md)
  3. Added changeset (patch)

Please let me know if anything else needs adjustment!

Copy link
Copy Markdown
Collaborator

@kimyouknow kimyouknow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! thanks for contributing

@kimyouknow kimyouknow merged commit 2a901bb into toss:main Mar 6, 2026
10 checks passed
@github-actions github-actions Bot mentioned this pull request Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants